<?php


namespace app\miniapp\controller\v1;


use app\common\cachemodel\AppQq;
use app\common\cachemodel\MiniAppUser;
use app\common\lib\exception\ApiException;
use app\common\lib\IAuth;
use app\common\lib\mini\alipay\AmpHelper;
use app\common\lib\Util;
use app\common\model\User;
use app\common\model\Wechat;
use app\miniapp\controller\Common;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use think\Cache;
use think\Log;
use think\response\Json;

class Login extends Common
{

    private const LOG_PRE = '小程序Login Controller 异常：';

    /**
     * 百度小程序保存到redis的access_token的key
     */
    private const SWAN_ACCESS_TOKEN_KEY = 'miniapp:swan_access_token';
    private const SWAN_SESSION_KEY = 'miniapp:session_key:swan';
    private const WEAPP_SESSION_KEY = 'miniapp:session_key:weapp';
    private const QQ_SESSION_KEY = 'miniapp:session_key:qq';
    private const TT_SESSION_KEY = 'miniapp:session_key:tt';

    /**
     * 登录接口
     * @throws ApiException
     * @throws Exception
     * @throws GuzzleException
     * @throws Exception
     */
    public function login()
    {
        $env_type = $this->request->header('app-type');
        $code = input('code/s');
        $app_type = input('app_type/s');

        $mini_item_conf = config('miniapp')[$env_type];
        //由于支付宝小程序的登录比较特殊，需要单做
        if ($env_type === 'ALIPAY')
        {
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'avatar' => input('avatar/s'),
                'nickname' => input('nickname/s'),
                'sex' => input('sex/d'),
                'city' => input('city/s'),
                'province' => input('province/s'),
                'country' => input('country_code/s'),
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('alipay')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
            //获取user_id，返回结果参考：https://docs.open.alipay.com/api_9/alipay.system.oauth.token
            $tokenData = AmpHelper::getAmpToken($code);
            if (!isset($tokenData['user_id']))
            {
                //记录支付宝通过code获取用户user_id失败的错误日志
                Log::write(self::LOG_PRE . json_encode($tokenData), 'error', true);
                return show(config('code.error'), '获取用户支付宝id失败!');
            }
            return $this->userLogin(array_merge($params, $tokenData));
        }

        $encrypted_data = input('encrypted_data/s');
        $iv = input('iv/s');
        //TT：字节跳动小程序；
        $tt_raw_data = input('tt_raw_data/s');
        $tt_signature = input('tt_signature/s');
        //TT：字节跳动小程序；

        if ('TT' === $env_type)
        {//字节跳动小程序
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'iv' => $iv,
                'encrypted_data' => $encrypted_data,
                'tt_raw_data' => $tt_raw_data,
                'tt_signature' => $tt_signature,
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('tt')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
        } else
        {//下面是除了支付宝/字节跳动小程序其他小程序
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'iv' => $iv,
                'encrypted_data' => $encrypted_data,
                'app_type' => $app_type,//准了星座 微信小程序，传入参数为3
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('others')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
        }

        $session_key = $this->getSessionKey($code, $env_type, $mini_item_conf);
        if (empty($session_key))
        {
            return show(config('code.error'), '获取session_key异常！');
        }

        switch ($env_type)
        {
            case 'TT':
                if ($session_key)
                {
                    //先校验数据是否合法
                    if ($tt_signature !== sha1($tt_raw_data . $session_key))
                    {
                        return false;
                    }
                    $data = $this->ttDecrypt($encrypted_data, $iv, $session_key);
                    if ($data)
                    {
                        return $this->userLogin(array_merge($params, $data), $session_key);
                    }
                }
                break;
            case 'SWAN':
                //百度小程序，解密数据，获取用户信息（注意：不包含unionid）
                $data = $this->swanDecrypt($encrypted_data, $iv, $mini_item_conf['app_key'], $session_key);
                if ($data)
                {
                    //获取unionid
                    //先查询redis缓存中是否存在access_token，如果存在那么使用，如果不存在，那么获取并放到redis缓存中
                    $access_token = Cache::get(self::SWAN_ACCESS_TOKEN_KEY);
                    if (empty($access_token) || $access_token === '')
                    {
                        //获取access_token（readme：https://smartprogram.baidu.com/docs/develop/serverapi/power_exp/）
                        $access_token_url = 'https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=' . $mini_item_conf['app_key'] . '&client_secret=' . $mini_item_conf['app_secret'] . '&scope=smartapp_snsapi_base';
                        $result_of_get_access_token = json_decode(file_get_contents($access_token_url), false);
                        if (isset($result_of_get_access_token->errcode))
                        {
                            Cache::rm(self::SWAN_ACCESS_TOKEN_KEY);
                        } else
                        {
                            $access_token = $result_of_get_access_token->access_token;
                            Cache::set(self::SWAN_ACCESS_TOKEN_KEY, $access_token, $result_of_get_access_token->expires_in);
                        }
                    }

                    //获取unionid（readme：https://smartprogram.baidu.com/docs/develop/api/open/log_getunionid/）
                    $union_url = 'https://openapi.baidu.com/rest/2.0/smartapp/getunionid?access_token=' . $access_token;
                    $body = ['openid' => $data['openid']];
                    $header = ['Content-Type' => 'application/x-www-form-urlencoded'];
                    $result_of_get_unionid = curl_post($union_url, $body, $header);
                    $result_of_get_unionid = json_decode($result_of_get_unionid, true);
                    if (isset($result_of_get_unionid['data'], $result_of_get_unionid['data']['unionid']))
                    {
                        $data_decrypt['unionid'] = $result_of_get_unionid['data']['unionid'];
                        return $this->userLogin(array_merge($data_decrypt, $data, $params), $session_key);
                    }

                    Cache::rm(self::SWAN_ACCESS_TOKEN_KEY);//如果错误，那么删除access_token，因为有可能是access_token错了
                } else
                {
                    return show(config('code.error'), 'failed');
                }
                break;
            case 'WEAPP':
            case 'QQ':
                $data = $this->wechatOrQQDecrypt($mini_item_conf['app_id'], $session_key, $encrypted_data, $iv);
                if ($data)
                {
                    return $this->userLogin(array_merge($params, $data), $session_key);
                }
                break;
        }

        return show(config('code.error'), '登录失败！');
    }

    /**
     * 用户登录统一处理
     * @param array $data
     * @param string $sessionKey
     * @return Json|null
     * @throws ApiException
     * @throws GuzzleException
     */
    private function userLogin($data = [], string $sessionKey = ''): ?Json
    {
        $third_id = '';

        $miniType = $data['mini_type'];
        $nickname = '';
        $avatar = '';
        $sex = '';
        $unionid = '';
        $openid = '';
        $city = '';
        $province = '';
        $country = '';
        $where = ['app_type' => 1, 'mini_type' => $miniType];
        if (isset($data['app_type']))
        {
            $where['app_type'] = $data['app_type'];
        }
        switch ($miniType)
        {
            case 'ALIPAY':
                $third_id = $data['user_id'];

                $nickname = $data['nickname'];
                $avatar = $data['avatar'];
                $sex = $data['sex'];
                $unionid = '';
                $openid = $third_id;
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                $where['openid'] = $data['user_id'];
                break;
            case 'QQ':
            case 'WEAPP':
                //todo 校验app、h5 微信的的unionid是否一样，准了科技的qq没有关联app应用，所以不能获取用户的unionid，所以这里有必要切换为星座女神的那个qq，否则未来没法做关联
                $third_id = $data['unionId'];

                $nickname = $data['nickName'];
                $avatar = $data['avatarUrl'];
                $sex = $data['gender'];
                $unionid = $data['unionId'];
                $openid = $data['openId'];
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                $where['unionid'] = $data['unionId'];
                break;
            case 'TT':
                $third_id = $data['openId'];

                $nickname = $data['nickName'];
                $avatar = $data['avatarUrl'];
                $sex = $data['gender'];
                $unionid = '';
                $openid = $third_id;
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                $where['openid'] = $data['openId'];
                break;
            case 'SWAN':
                $third_id = $data['unionid'];

                $nickname = $data['nickname'];
                $avatar = $data['headimgurl'];
                $sex = $data['sex'];
                $unionid = $third_id;
                $openid = $data['openid'];
                $city = '';
                $province = '';
                $country = '';
                $where['unionid'] = $data['unionId'];
                break;
        }

        try
        {

            $MiniAppUser = new MiniAppUser();
            //验证是否注册过
            $mini_user = $MiniAppUser->where($where)->order('id desc')->find();
            if (!$mini_user || $mini_user['status'] == 1)
            {//如果用户没有注册 或者 注册用户已经注销了 ==> 注册账户
                //生成access_token和过期时间
                $tokenData = $this->getUserTokenInfo($third_id);
                $avatarInfo = null;
                if ($avatar)
                {
                    $avatarInfo = Util::wxImgUploadOss($avatar, 'uploads/avatar');
                    if (false === $avatarInfo['success'])
                    {
                        Log::write(self::LOG_PRE . '  上传三方头像失败，' . $avatarInfo['message'], 'error');
                    }
                }
                $avatar = $avatarInfo['success'] ? $avatarInfo['url'] : $avatar;

                $mini_user = [
                    'mini_type' => $miniType,
                    'nickname' => $nickname ?? '',
                    'avatar' => $avatar,
                    'sex' => $sex,
                    'unionid' => $unionid,
                    'openid' => $openid,
                    'city' => $city,
                    'province' => $province,
                    'country' => $country,
                    'access_token' => $tokenData['access_token'],
                    'expires_in' => $tokenData['expires_in'],
                    'create_time' => time(),
                    'app_type' => 1,
                    'status' => 0,
                    'uid' => 0,
                    'channel' => (int)($this->headers['channel'] ?? 0) // 渠道ID
                ];

                switch ($miniType)
                {
                    case 'WEAPP':
                        // 判断用户是否在APP使用过微信登陆 生成过账户
                        $wechatInfo = (new Wechat())->alias('w')
                            ->field('w.uid')
                            ->join('app_user u', 'u.id=w.uid', 'INNER')
                            ->where(['w.unionid' => $unionid, 'u.forbidden' => 0])->find();
                        if ($wechatInfo)
                        {   // 如果用户注册过
                            $mini_user['uid'] = $wechatInfo['uid'];
                        } else if ($this->headers['version'] !== '1.0.0')
                        {// 如果用户没有注册过，那么给用户注册一下（微信小程序可以不绑定手机号）

                            $ip = $this->request->ip();
                            $time = $this->request->time();

                            // 第一次登录 注册数据
                            $user['uen'] = substr(md5($this->request->time(true) . mt_rand(0, 1000000) . uniqid()), 0, 12);
                            $user['nickname'] = $nickname ?? '';
                            $user['reg_time'] = $time;
                            $user['reg_ip'] = $ip;
                            $user['last_login'] = $time;
                            $user['last_ip'] = $ip;
                            $user['last_active'] = $time;
                            $user['status'] = 1;
                            $user['sex'] = 1;
                            $user['is_first_login'] = 1;
                            $user['has_self_recode'] = 0;
                            $user['uc_id'] = md5($this->request->time(true));
                            $user['channel'] = Util::getChannelId();
                            try
                            {
                                $uid = (new \app\common\cachemodel\User())->insertGetId($user);
                                $user['id'] = $uid;
                                $mini_user['uid'] = $uid;

                                // 微信小程序绑定手机号 微信账户未注册->注册微信登陆账户
                                $wechat_insert = [
                                    'uid' => $uid,
                                    'nickname' => $mini_user['nickname'],
                                    'sex' => $mini_user['sex'],
                                    'city' => $mini_user['city'],
                                    'province' => $mini_user['province'],
                                    'country' => $mini_user['country'],
                                    'headimgurl' => $mini_user['avatar'],
                                    'openid' => $openid,
                                    'unionid' => $mini_user['unionid'],
                                    'create_time' => $time,
                                    'update_time' => $time,
                                ];
                                (new \app\common\cachemodel\Wechat())->SaveData($wechat_insert);
                            }
                            catch (\Exception $e)
                            {
                                Log::write(self::LOG_PRE . '微信小程序用户注册/登录失败，' . $e->getMessage() . '失败行数：' . $e->getLine(), 'error');
                                return show(config('code.error'), '注册失败,请稍后重试！');
                            }

                        }
                        break;
                    case 'QQ':
                        // 判断用户是否在APP使用过QQ登陆 生成过账户
                        $qqInfo = (new AppQq())->alias('q')
                            ->field('u.id uid')
                            ->join('wechat_backup_07_18_14_43_00.app_user u', 'q.uid=u.id')
                            ->where(['q.unionid' => $unionid, 'u.forbidden' => 0])->find();
                        if ($qqInfo)
                        {   // 如果用户注册过 且有自己的档案
                            $mini_user['uid'] = $qqInfo['uid'];
                        }
                        break;
                }
                $id = $MiniAppUser->SavaData($mini_user);
                $mini_user['id'] = $id;
            } elseif ($mini_user && $mini_user['status'] == 2)
            {   // 用户已被禁止登陆
                throw new ApiException('该账号禁止登录');
            } else
            {   // 账户登陆
                //生成access_token和过期时间
                $tokenData = $this->getUserTokenInfo($third_id);
                // 更新数据库缓存信息
                $MiniAppUser->save($tokenData, ['id' => $mini_user['id']]);
            }

            if ($mini_user['uid'])
            {
                $userInfo = (new User())->getUserinfo(['id' => $mini_user['uid']]);
                if ($userInfo['forbidden'] == 1)
                {
                    throw new ApiException('该账号禁止登录');
                }
                $rid = (int)($userInfo['rid'] ?? 0);  // 是否有自己的档案

                $hasBindMobile = $userInfo['tel'] ? 1 : 0; // 是否绑定手机
            } else
            {
                $rid = 0;
                $hasBindMobile = 0;
            }

            //微信小程序，直接设置已经绑定了手机号
            if ($miniType === 'WEAPP' && $this->headers['version'] !== '1.0.0')
            {
                $hasBindMobile = 1;
            }

            // 账号模拟
            $simulation = config('app.simulation');
            $uid = (int)$mini_user['uid'];
            if ($uid !== $simulation['from'])
            {
                $resUid = $uid;
            } else
            {
                $resUid = $simulation['to'] ?: $uid;
            }

            if ($uid !== $resUid)
            {
                $simulationUserInfo = (new \app\common\model\User())->getUserinfo(['id' => $resUid]);
                if ($simulationUserInfo['forbidden'] == 1)
                {
                    throw new ApiException('该账号禁止登录', 200, 0);
                }

                $rid = (int)($simulationUserInfo['rid'] ?? 0);  // 是否有自己的档案
            }

            $data = [
                'rid' => $rid,    // 档案id 0没有
                'has_bind_phone' => $hasBindMobile, // 是否绑定手机 0未绑定 1已绑定
                'access_token' => $this->encryptWithOpenssl($tokenData['access_token'] . "||" . $mini_user['id']),
                'mid' => $mini_user['id']
            ];

            if ($sessionKey)
            {
                $CacheSessionKey = $miniType . '_SESSION_KEY';
                Cache::set(constant('self::' . $CacheSessionKey) . ':' . $data['mid'], $sessionKey, 300);
            }

            return show(config('code.success'), '登录成功', $data);
        }
        catch (Exception $e)
        {
            Log::write(self::LOG_PRE . '用户注册/登录失败，' . $e->getMessage() . '失败行数：' . $e->getLine(), 'error');
            throw new ApiException('登录失败', 200, 0);
        }
    }

    /**
     * 获取用户token信息
     * @param string $third_id
     * @return array
     */
    public function getUserTokenInfo($third_id = ''): array
    {
        // 生成token  和 token到期时间
        $token = IAuth::setAppLoginToken($third_id);
        $data = [
            'access_token' => $token,
            'expires_in' => strtotime('+' . config('miniapp.login_time_out_day') . ' days'),
        ];
        return $data;
    }
}
